﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Shapes;
using PetriNetSharp.Engine;
using System.Diagnostics;
using System.Windows.Data;
using PetriNetSharp.Converter;
using System.Windows.Input;
using System.Windows;
using System.Windows.Media;
using System.Windows.Controls;
using PetriNetSharp.CustomUIControls;
using System.Windows.Media.Effects;
using System.Windows.Media.Animation;

namespace PetriNetSharp.Controller
{
	/// <summary>
	/// The controller class that connects the Arc model and the Arc view
	/// </summary>
	public class ArcController : IControllerInvalidatable
	{
		/// <summary>
		/// The stroke thickness for the arc
		/// </summary>
		public const int ARC_STROKE_THICKNESS = 4;

		/// <summary>
		/// The view for the arc
		/// </summary>
		private Line view;

		/// <summary>
		/// The model for the arc
		/// </summary>
		private Arc model;

		/// <summary>
		/// The converter for binding the view's points
		/// </summary>
		private PointToDoubleValueConverter pointConverter = 
			new PointToDoubleValueConverter();

		/// <summary>
		/// The context menu for the view when it is being modified
		/// </summary>
		private ContextMenu modifyContextMenu;

		/// <summary>
		/// The delete menu item for the view's context menu
		/// </summary>
		private MenuItem deleteMenuItem;

		/// <summary>
		/// The tool tip for this arc
		/// </summary>
		private SharpToolTip toolTip;

		/// <summary>
		/// The effect used to show this arc has had its transitions fire.
		/// </summary>
		private BlurEffect firedEffect;

		/// <summary>
		/// Duration for when animation on the fire highlightn show and hide
		/// </summary>
		/// <remarks>
		/// Currently a duration of 1/2 seconds
		/// </remarks>
		private Duration firedDuration = new Duration(new TimeSpan(0, 0, 0, 0, 100));

		/// <summary>
		/// The radius for the fired effect when showing
		/// </summary>
		private const double SHOW_FIRED_RADIUS = 3;

		/// <summary>
		/// The radius for the fired effect when hiding
		/// </summary>
		private const double HIDE_FIRED_RADIUS = 0;

		/// <summary>
		/// Create a new ArcController
		/// </summary>
		/// <param name="view">The View for this arc</param>
		/// <param name="model">The model for this arc</param>
		/// <preconditions>
		/// The parameter view cannot be null
		/// The parameter model cannot be null
		/// </preconditions>
		public ArcController(Line view, Arc model)
		{
			#region preconditions
			Debug.Assert(view != null, "ArcController parameter view cannot be null", "The parameter view for ArcController cannot be null for the constructor");
			Debug.Assert(model != null, "ArcController parameter model cannot be null", "The parameter model for ArcController cannot be null for the constructor");
			#endregion

			modifyContextMenu = new ContextMenu();

			//Set up the delete menu item so the user can delete this arc
			deleteMenuItem = new MenuItem();
			deleteMenuItem.Header = "Delete Arc";
			deleteMenuItem.Click += (object sender, RoutedEventArgs e) =>
				{
					Model.Invalidate();
				};
			SharpToolTip deleteToolTip = new SharpToolTip();
			deleteToolTip.Header = "Delete Arc";
			deleteToolTip.Text = "Click this to delete this arc from the Petri Net.";
			deleteMenuItem.ToolTip = deleteToolTip;
			modifyContextMenu.Items.Add(deleteMenuItem);
			toolTip = new SharpToolTip();

			this.view = view;
			this.model = model;

			firedEffect = new BlurEffect();
			firedEffect.RenderingBias = RenderingBias.Performance;
			firedEffect.KernelType = KernelType.Box;
			firedEffect.Radius = HIDE_FIRED_RADIUS;

			RemoveConnections();
			SetConnections();
		}


		/// <summary>
		/// Get or set the arc view
		/// </summary>
		/// <preconditions>
		/// On set, value passed in cannot be null
		/// </preconditions>
		public Line View
		{
			get { return view; }
			set
			{
				#region preconditions
				Debug.Assert(value != null, "View cannot be null", "The ArcController was passed a null value for its view");
				#endregion

				RemoveConnections();
				view = value;
				RemoveConnections();
				SetConnections();
			}
		}

		/// <summary>
		/// Get or set the arc model
		/// </summary>
		/// <preconditions>
		/// On set, value passed in cannot be null
		/// </preconditions>
		public Arc Model
		{
			get { return model; }
			set
			{
				#region preconditions
				Debug.Assert(value != null, "Model cannot be null", "The ArcController was passed a null value for its model");
				#endregion
				
				RemoveConnections();
				model = value;
				RemoveConnections();
				SetConnections();
			}
		}

		#region set and remove connections from model to view 

		/// <summary>
		/// Removes all connections between the view and model
		/// </summary>
		private void RemoveConnections()
		{
			//Remove from focus scope
			FocusManager.SetIsFocusScope(View, false);
			View.Focusable = false;
			View.GotFocus -= ViewFocusGained;
			View.LostFocus -= ViewFocusLost;
			View.MouseDown -= ViewMouseDown;

			//Clear the bindings to the line ends
			if(BindingOperations.IsDataBound(View, Line.X1Property))
			{
				BindingOperations.ClearBinding(View, Line.X1Property);
			}
			if (BindingOperations.IsDataBound(View, Line.X2Property))
			{
				BindingOperations.ClearBinding(View, Line.X2Property);
			}
			if (BindingOperations.IsDataBound(View, Line.Y1Property))
			{
				BindingOperations.ClearBinding(View, Line.Y1Property);
			}
			if (BindingOperations.IsDataBound(View, Line.Y2Property))
			{
				BindingOperations.ClearBinding(View, Line.Y2Property);
			}

			//Remove the View's tool tip
			View.ToolTip = null;

			//Remove the View's context menu
			View.ContextMenu = null;

			//Remove the Model's invalidation listener
			Model.Invalidated -= Invalidate;

			//Remove the listeners on the Model's start and end name changes
			Model.Start.NameChanged -= SetToolTipText;
			Model.End.NameChanged -= SetToolTipText;

			//Remove the View's effect
			View.Effect = null;

			//Remove the show fired effect to the transitions fired event
			Model.Transition.Fired -= ShowFireHighlight;
		}

		/// <summary>
		/// Sets the connections between the view and the model
		/// </summary>
		private void SetConnections()
		{
			//Add focus functionality
			FocusManager.SetIsFocusScope(View, true);
			View.Focusable = true;
			View.GotFocus += ViewFocusGained;
			View.LostFocus += ViewFocusLost;
			View.MouseDown += ViewMouseDown;

			Binding pointBind = new Binding();

			//Bind X1
			pointBind.Source = Model.Start;
			pointBind.Path = new System.Windows.PropertyPath("Location");
			pointBind.Converter = pointConverter;
			pointBind.ConverterParameter = PointToDoubleValueConverterParams.X;
			View.SetBinding(Line.X1Property, pointBind);

			//Bind Y1
			pointBind = new Binding();
			pointBind.Source = Model.Start;
			pointBind.Path = new System.Windows.PropertyPath("Location");
			pointBind.Converter = pointConverter;
			pointBind.ConverterParameter = PointToDoubleValueConverterParams.Y;
			View.SetBinding(Line.Y1Property, pointBind);

			//Bind X2
			pointBind = new Binding();
			pointBind.Source = Model.End;
			pointBind.Path = new System.Windows.PropertyPath("Location");
			pointBind.Converter = pointConverter;
			pointBind.ConverterParameter = PointToDoubleValueConverterParams.X;
			View.SetBinding(Line.X2Property, pointBind);

			//Bind Y2
			pointBind = new Binding();
			pointBind.Source = this.Model.End;
			pointBind.Path = new System.Windows.PropertyPath("Location");
			pointBind.Converter = pointConverter;
			pointBind.ConverterParameter = PointToDoubleValueConverterParams.Y;
			View.SetBinding(Line.Y2Property, pointBind);

			//Attach the View's context menu
			View.ContextMenu = modifyContextMenu;

			//Set up the View's tool tip
			if (Model.IsInputArc)
				toolTip.Header = "Input Arc";
			else
				toolTip.Header = "Output Arc";
			SetToolTipText();
			View.ToolTip = toolTip;


			View.Effect = firedEffect;

			//Attach the Model's invalidated listener
			Model.Invalidated += Invalidate;

			//Attach the listeners on the Model's start and end name changes so the tool tip can be updated
			Model.Start.NameChanged += SetToolTipText;
			Model.End.NameChanged += SetToolTipText;

			//Attach Is Running listeners so the View can be updated
			Model.Transition.IsRunningChanged += TransitionIsRunningChangedListener;

			//Attach the fired effect to the transitions fired event
			Model.Transition.Fired += ShowFireHighlight;

			//Set the View's colors
			if (Model.IsInputArc)
				View.SetResourceReference(Line.StrokeProperty, "InputArcBrush");
			else
				View.SetResourceReference(Line.StrokeProperty, "OutputArcBrush");

			View.StrokeThickness = ARC_STROKE_THICKNESS;
		}

		/// <summary>
		/// Listens for when the Arc's transitions is running and updates
		/// the view
		/// </summary>
		/// <remarks>
		/// The use of Transition is arbitrary, the Place could also be used
		/// </remarks>
		private void TransitionIsRunningChangedListener()
		{
			if (Model.Transition.IsRunning)
				View.ContextMenu = null;
			else
				View.ContextMenu = modifyContextMenu;
		}

		/// <summary>
		/// Sets the tool tip's text to be in the format:
		/// Connects ## to ##
		/// </summary>
		private void SetToolTipText()
		{
			toolTip.Text = String.Format("Connects: {0} to {1}", Model.Start.Name, Model.End.Name);
		}

		#endregion

		#region focus functionality

		/// <summary>
		/// Listens for when the View gains focus and performs the visual changes
		/// </summary>
		/// <param name="sender">Not used</param>
		/// <param name="e">Not used</param>
		private void ViewFocusGained(object sender, RoutedEventArgs e)
		{
			View.SetResourceReference(Line.StrokeProperty, "SelectedArcBrush");
		}

		/// <summary>
		/// Listens for when the View loses focus and performs the visual changes
		/// </summary>
		/// <param name="sender">Not used</param>
		/// <param name="e">Not used</param>
		private void ViewFocusLost(object sender, RoutedEventArgs e)
		{
			if (Model.IsInputArc)
				View.SetResourceReference(Line.StrokeProperty, "InputArcBrush");
			else if (Model.IsOutputArc)
				View.SetResourceReference(Line.StrokeProperty, "OutputArcBrush");
		}

		/// <summary>
		/// Listens for when the View has a mouse button is down and give the View focus
		/// </summary>
		/// <param name="sender">Not used</param>
		/// <param name="e">Not used</param>
		private void ViewMouseDown(object sender, MouseButtonEventArgs e)
		{
			FocusManager.SetFocusedElement(View.Parent, View);
		}

		#endregion

		#region fire highligthing funcitonality

		/// <summary>
		/// Shows the fire highlighting effect on the View
		/// </summary>
		public void ShowFireHighlight()
		{
			DoubleAnimation fireAnimation = new DoubleAnimation();
			fireAnimation.To = SHOW_FIRED_RADIUS;
			fireAnimation.Duration = firedDuration;
			firedEffect.BeginAnimation(BlurEffect.RadiusProperty, fireAnimation);
		}

		/// <summary>
		/// Get rid of the fire highlighting effect on the View
		/// </summary>
		public void HideFireHighlight()
		{
			DoubleAnimation fireAnimation = new DoubleAnimation();
			fireAnimation.To = HIDE_FIRED_RADIUS;
			fireAnimation.Duration = firedDuration;
			firedEffect.BeginAnimation(BlurEffect.RadiusProperty, fireAnimation);
		}

		#endregion

		#region IControllerInvalidatable Members

		/// <summary>
		/// Occurs when this ArcController is invalidated
		/// </summary>
		public event ControllerInvalidatedEventHandler Invalidated;

		/// <summary>
		/// Invalidate this ArcController
		/// </summary>
		public void Invalidate()
		{
			if (Invalidated != null)
				Invalidated(this);
		}

		#endregion
	}
}
